Completed
Push — master ( 789c09...13763e )
by Ruben de
05:19 queued 03:08
created

Wallet.pay   D

Complexity

Conditions 1
Paths 3

Size

Total Lines 41

Duplication

Lines 1
Ratio 2.44 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
nc 3
dl 1
loc 41
rs 4.5365
nop 0
1
var _ = require('lodash');
2
var assert = require('assert');
3
var q = require('q');
4
var async = require('async');
5
var bitcoin = require('bitcoinjs-lib');
6
var bitcoinMessage = require('bitcoinjs-message');
7
var blocktrail = require('./blocktrail');
8
var CryptoJS = require('crypto-js');
9
var Encryption = require('./encryption');
10
var EncryptionMnemonic = require('./encryption_mnemonic');
11
var SizeEstimation = require('./size_estimation');
12
var bip39 = require('bip39');
13
14
var SignMode = {
15
    SIGN: "sign",
16
    DONT_SIGN: "dont_sign"
17
};
18
19
/**
20
 *
21
 * @param sdk                   APIClient       SDK instance used to do requests
22
 * @param identifier            string          identifier of the wallet
23
 * @param walletVersion         string
24
 * @param primaryMnemonic       string          primary mnemonic
25
 * @param encryptedPrimarySeed
26
 * @param encryptedSecret
27
 * @param primaryPublicKeys     string          primary mnemonic
28
 * @param backupPublicKey       string          BIP32 master pubKey M/
29
 * @param blocktrailPublicKeys  array           list of blocktrail pubKeys indexed by keyIndex
30
 * @param keyIndex              int             key index to use
31
 * @param segwit                int             segwit toggle from server
32
 * @param testnet               bool            testnet
33
 * @param checksum              string
34
 * @param upgradeToKeyIndex     int
35
 * @param bypassNewAddressCheck bool            flag to indicate if wallet should/shouldn't derive new address locally to verify api
36
 * @constructor
37
 * @internal
38
 */
39
var Wallet = function(
40
    sdk,
41
    identifier,
42
    walletVersion,
43
    primaryMnemonic,
44
    encryptedPrimarySeed,
45
    encryptedSecret,
46
    primaryPublicKeys,
47
    backupPublicKey,
48
    blocktrailPublicKeys,
49
    keyIndex,
50
    segwit,
51
    testnet,
52
    checksum,
53
    upgradeToKeyIndex,
54
    bypassNewAddressCheck
55
) {
56
    /* jshint -W071 */
57
    var self = this;
58
59
    self.sdk = sdk;
60
    self.identifier = identifier;
61
    self.walletVersion = walletVersion;
62
    self.locked = true;
63
    self.bypassNewAddressCheck = !!bypassNewAddressCheck;
64
    self.bitcoinCash = self.sdk.bitcoinCash;
65
    self.segwit = !!segwit;
66
    assert(self.segwit === 0 || !self.bitcoinCash);
67
68
    self.testnet = testnet;
69
    if (self.bitcoinCash) {
70
        if (self.testnet) {
71
            self.network = bitcoin.networks.bitcoincashtestnet;
72
        } else {
73
            self.network = bitcoin.networks.bitcoincash;
74
        }
75
    } else {
76
        if (self.testnet) {
77
            self.network = bitcoin.networks.testnet;
78
        } else {
79
            self.network = bitcoin.networks.bitcoin;
80
        }
81
    }
82
83
    assert(backupPublicKey instanceof bitcoin.HDNode);
84
    assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; }));
85
    assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; }));
86
87
    // v1
88
    self.primaryMnemonic = primaryMnemonic;
89
90
    // v2 & v3
91
    self.encryptedPrimarySeed = encryptedPrimarySeed;
92
    self.encryptedSecret = encryptedSecret;
93
94
    self.primaryPrivateKey = null;
95
    self.backupPrivateKey = null;
96
97
    self.backupPublicKey = backupPublicKey;
98
    self.blocktrailPublicKeys = blocktrailPublicKeys;
99
    self.primaryPublicKeys = primaryPublicKeys;
100
    self.keyIndex = keyIndex;
101
102
    if (!self.bitcoinCash) {
103
        if (self.segwit) {
104
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
105
            self.changeChain = Wallet.CHAIN_BTC_SEGWIT;
106
        } else {
107
            self.chain = Wallet.CHAIN_BTC_DEFAULT;
108
            self.changeChain = Wallet.CHAIN_BTC_DEFAULT;
109
        }
110
    } else {
111
        self.chain = Wallet.CHAIN_BCC_DEFAULT;
112
        self.changeChain = Wallet.CHAIN_BCC_DEFAULT;
113
    }
114
115
    self.checksum = checksum;
116
    self.upgradeToKeyIndex = upgradeToKeyIndex;
117
118
    self.secret = null;
119
    self.seedHex = null;
120
};
121
122
Wallet.WALLET_VERSION_V1 = 'v1';
123
Wallet.WALLET_VERSION_V2 = 'v2';
124
Wallet.WALLET_VERSION_V3 = 'v3';
125
126
Wallet.WALLET_ENTROPY_BITS = 256;
127
128
Wallet.OP_RETURN = 'opreturn';
129
Wallet.DATA = Wallet.OP_RETURN; // alias
130
131
Wallet.PAY_PROGRESS_START = 0;
132
Wallet.PAY_PROGRESS_COIN_SELECTION = 10;
133
Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20;
134
Wallet.PAY_PROGRESS_SIGN = 30;
135
Wallet.PAY_PROGRESS_SEND = 40;
136
Wallet.PAY_PROGRESS_DONE = 100;
137
138
Wallet.CHAIN_BTC_DEFAULT = 0;
139
Wallet.CHAIN_BTC_SEGWIT = 2;
140
Wallet.CHAIN_BCC_DEFAULT = 1;
141
142
Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE;
143
Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE;
144
Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL;
145
Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY;
146
Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE;
147
148
Wallet.prototype.isSegwit = function() {
149
    return !!this.segwit;
150
};
151
152
Wallet.prototype.unlock = function(options, cb) {
153
    var self = this;
154
155
    var deferred = q.defer();
156
    deferred.promise.nodeify(cb);
157
158
    // avoid modifying passed options
159
    options = _.merge({}, options);
160
161
    q.fcall(function() {
162
        switch (self.walletVersion) {
163
            case Wallet.WALLET_VERSION_V1:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
164
                return self.unlockV1(options);
165
166
            case Wallet.WALLET_VERSION_V2:
167
                return self.unlockV2(options);
168
169
            case Wallet.WALLET_VERSION_V3:
170
                return self.unlockV3(options);
171
172
            default:
173
                return q.reject(new blocktrail.WalletInitError("Invalid wallet version"));
174
        }
175
    }).then(
176
        function(primaryPrivateKey) {
177
            self.primaryPrivateKey = primaryPrivateKey;
178
179
            // create a checksum of our private key which we'll later use to verify we used the right password
180
            var checksum = self.primaryPrivateKey.getAddress();
181
182
            // check if we've used the right passphrase
183
            if (checksum !== self.checksum) {
184
                throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " +
185
                    "[" + self.checksum + "], most likely due to incorrect password");
186
            }
187
188
            self.locked = false;
189
190
            // if the response suggests we should upgrade to a different blocktrail cosigning key then we should
191
            if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if typeof self.upgradeToKey...radeToKeyIndex !== null is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
192
                return self.upgradeKeyIndex(self.upgradeToKeyIndex);
193
            }
194
        }
195
    ).then(
196
        function(r) {
197
            deferred.resolve(r);
198
        },
199
        function(e) {
200
            deferred.reject(e);
201
        }
202
    );
203
204
    return deferred.promise;
205
};
206
207
Wallet.prototype.unlockV1 = function(options) {
208
    var self = this;
209
210
    options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic;
211
    options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic;
212
213
    return self.sdk.resolvePrimaryPrivateKeyFromOptions(options)
214
        .then(function(options) {
215
            self.primarySeed = options.primarySeed;
216
217
            return options.primaryPrivateKey;
218
        });
219
};
220
221
Wallet.prototype.unlockV2 = function(options, cb) {
222
    var self = this;
223
224
    var deferred = q.defer();
225
    deferred.promise.nodeify(cb);
226
227
    deferred.resolve(q.fcall(function() {
228
        /* jshint -W071, -W074 */
229
        options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
230
        options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
231
232
        if (options.secret) {
233
            self.secret = options.secret;
234
        }
235
236
        if (options.primaryPrivateKey) {
237
            throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated");
238
        }
239
240
        if (options.primarySeed) {
241
            self.primarySeed = options.primarySeed;
242
        } else if (options.secret) {
243
            try {
244
                self.primarySeed = new Buffer(
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
245
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
246
                if (!self.primarySeed.length) {
247
                    throw new Error();
248
                }
249
            } catch (e) {
250
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
251
            }
252
253
        } else {
254
            // avoid conflicting options
255
            if (options.passphrase && options.password) {
256
                throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
257
            }
258
            // normalize passphrase/password
259
            options.passphrase = options.passphrase || options.password;
260
261
            try {
262
                self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8);
263
                if (!self.secret.length) {
264
                    throw new Error();
265
                }
266
            } catch (e) {
267
                throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
268
            }
269
            try {
270
                self.primarySeed = new Buffer(
271
                    CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64');
272
                if (!self.primarySeed.length) {
273
                    throw new Error();
274
                }
275
            } catch (e) {
276
                throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
277
            }
278
        }
279
280
        return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
281
    }));
282
283
    return deferred.promise;
284
};
285
286
Wallet.prototype.unlockV3 = function(options, cb) {
287
    var self = this;
288
289
    var deferred = q.defer();
290
    deferred.promise.nodeify(cb);
291
292
    deferred.resolve(q.fcall(function() {
293
        return q.when()
294
            .then(function() {
295
                /* jshint -W071, -W074 */
296
                options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed;
297
                options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret;
298
299
                if (options.secret) {
300
                    self.secret = options.secret;
301
                }
302
303
                if (options.primaryPrivateKey) {
304
                    throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated");
305
                }
306
307
                if (options.primarySeed) {
308
                    self.primarySeed = options.primarySeed;
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
309
                } else if (options.secret) {
310
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
311
                        .then(function(primarySeed) {
312
                            self.primarySeed = primarySeed;
313
                        }, function() {
314
                            throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
315
                        });
316
                } else {
317
                    // avoid conflicting options
318
                    if (options.passphrase && options.password) {
319
                        throw new blocktrail.WalletCreateError("Can't specify passphrase and password");
320
                    }
321
                    // normalize passphrase/password
322
                    options.passphrase = options.passphrase || options.password;
323
                    delete options.password;
324
325
                    return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase))
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
326
                        .then(function(secret) {
327
                            self.secret = secret;
328
                        }, function() {
329
                            throw new blocktrail.WalletDecryptError("Failed to decrypt secret");
330
                        })
331
                        .then(function() {
332
                            return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret)
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
333
                                .then(function(primarySeed) {
334
                                    self.primarySeed = primarySeed;
335
                                }, function() {
336
                                    throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed");
337
                                });
338
                        });
339
                }
340
            })
341
            .then(function() {
342
                return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network);
343
            })
344
        ;
345
    }));
346
347
    return deferred.promise;
348
};
349
350
Wallet.prototype.lock = function() {
351
    var self = this;
352
353
    self.secret = null;
354
    self.primarySeed = null;
355
    self.primaryPrivateKey = null;
356
    self.backupPrivateKey = null;
357
358
    self.locked = true;
359
};
360
361
/**
362
 * upgrade wallet to V3 encryption scheme
363
 *
364
 * @param passphrase is required again to reencrypt the data, important that it's the correct password!!!
365
 * @param cb
366
 * @returns {promise}
367
 */
368
Wallet.prototype.upgradeToV3 = function(passphrase, cb) {
369
    var self = this;
370
371
    var deferred = q.defer();
372
    deferred.promise.nodeify(cb);
373
374
    q.when(true)
375
        .then(function() {
376
            if (self.locked) {
377
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade");
378
            }
379
380
            if (self.walletVersion === Wallet.WALLET_VERSION_V3) {
381
                throw new blocktrail.WalletUpgradeError("Wallet is already V3");
382
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
383
                return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred));
384
            } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if self.walletVersion === Wallet.WALLET_VERSION_V1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
385
                return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred));
386
            }
387
        })
388
        .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); });
389
390
    return deferred.promise;
391
};
392
393
Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) {
394
    var self = this;
395
396
    return q.when(true)
397
        .then(function() {
398
            var options = {
399
                storeDataOnServer: true,
400
                passphrase: passphrase,
401
                primarySeed: self.primarySeed,
402
                recoverySecret: false // don't create new recovery secret, V2 already has ones
403
            };
404
405
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
406
                .then(function(options) {
407
                    return self.sdk.updateWallet(self.identifier, {
408
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
409
                        encrypted_secret: options.encryptedSecret.toString('base64'),
410
                        wallet_version: Wallet.WALLET_VERSION_V3
411
                    }).then(function() {
412
                        self.secret = options.secret;
413
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
414
                        self.encryptedSecret = options.encryptedSecret;
415
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
416 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
417
                        return self;
418
                    });
419
                });
420
        });
421
422
};
423
424
Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) {
425
    var self = this;
426
427
    return q.when(true)
428
        .then(function() {
429
            var options = {
430
                storeDataOnServer: true,
431
                passphrase: passphrase,
432
                primarySeed: self.primarySeed
433
            };
434
435
            return self.sdk.produceEncryptedDataV3(options, notify || function noop() {})
436
                .then(function(options) {
437
                    // store recoveryEncryptedSecret for printing on backup sheet
438
                    self.recoveryEncryptedSecret = options.recoveryEncryptedSecret;
439
440
                    return self.sdk.updateWallet(self.identifier, {
441
                        primary_mnemonic: '',
442
                        encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'),
443
                        encrypted_secret: options.encryptedSecret.toString('base64'),
444
                        recovery_secret: options.recoverySecret.toString('hex'),
445
                        wallet_version: Wallet.WALLET_VERSION_V3
446
                    }).then(function() {
447
                        self.secret = options.secret;
448
                        self.encryptedPrimarySeed = options.encryptedPrimarySeed;
449
                        self.encryptedSecret = options.encryptedSecret;
450
                        self.walletVersion = Wallet.WALLET_VERSION_V3;
451
452
                        return self;
453
                    });
454
                });
455
        });
456
};
457
458
Wallet.prototype.doPasswordChange = function(newPassword) {
459
    var self = this;
460
461
    return q.when(null)
462
        .then(function() {
463
464
            if (self.walletVersion === Wallet.WALLET_VERSION_V1) {
465
                throw new blocktrail.WalletLockedError("Wallet version does not support password change!");
466
            }
467
468
            if (self.locked) {
469
                throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password");
470
            }
471
472
            if (!self.secret) {
473
                throw new blocktrail.WalletLockedError("No secret");
474
            }
475
476
            var newEncryptedSecret;
477
            var newEncrypedWalletSecretMnemonic;
478
            if (self.walletVersion === Wallet.WALLET_VERSION_V2) {
479
                newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL);
480
                newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex'));
481
482
            } else {
483
                if (typeof newPassword === "string") {
484
                    newPassword = new Buffer(newPassword);
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
485
                } else {
486
                    if (!(newPassword instanceof Buffer)) {
487
                        throw new Error('New password must be provided as a string or a Buffer');
488
                    }
489
                }
490
491
                newEncryptedSecret = Encryption.encrypt(self.secret, newPassword);
492
                newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret);
493
494
                // It's a buffer, so convert it back to base64
495
                newEncryptedSecret = newEncryptedSecret.toString('base64');
496
            }
497
498
            return [newEncryptedSecret, newEncrypedWalletSecretMnemonic];
499
        });
500
};
501
502
Wallet.prototype.passwordChange = function(newPassword, cb) {
503
    var self = this;
504
505
    var deferred = q.defer();
506
    deferred.promise.nodeify(cb);
507
508
    q.fcall(function() {
509
        return self.doPasswordChange(newPassword)
510
            .then(function(r) {
511
                var newEncryptedSecret = r[0];
512
                var newEncrypedWalletSecretMnemonic = r[1];
513
514
                return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() {
515
                    self.encryptedSecret = newEncryptedSecret;
516
517
                    // backupInfo
518
                    return {
519
                        encryptedSecret: newEncrypedWalletSecretMnemonic
520
                    };
521
                });
522
            })
523
            .then(
524
                function(r) {
525
                    deferred.resolve(r);
526
                },
527
                function(e) {
528
                    deferred.reject(e);
529
                }
530
            );
531
    });
532
533
    return deferred.promise;
534
};
535
536
/**
537
 * get address for specified path
538
 *
539
 * @param path
540
 * @returns string
541
 */
542
Wallet.prototype.getAddressByPath = function(path) {
543
    return this.getWalletScriptByPath(path).address;
544
};
545
546
/**
547
 * get redeemscript for specified path
548 View Code Duplication
 *
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
549
 * @param path
550
 * @returns {bitcoin.Script}
551
 */
552
Wallet.prototype.getRedeemScriptByPath = function(path) {
553
    return this.getWalletScriptByPath(path).redeemScript;
554
};
555
556
Wallet.prototype.getWalletScriptByPath = function(path) {
557
    var self = this;
558
559
    // get derived primary key
560
    var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path);
561
    // get derived blocktrail key
562
    var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path);
563
    // derive the backup key
564
    var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M");
565
566
    // sort the pubkeys
567
    var pubKeys = Wallet.sortMultiSigKeys([
568
        derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(),
569
        derivedBackupPublicKey.keyPair.getPublicKeyBuffer(),
570
        derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer()
571
    ]);
572
573
    var multisig = bitcoin.script.multisig.output.encode(2, pubKeys);
574
    var scriptType = parseInt(path.split("/")[2]);
575
576
    var ws, rs;
577
    if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) {
578
        ws = multisig;
579
        rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws));
580
    } else {
581
        ws = null;
582
        rs = multisig;
583
    }
584
585
    var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs));
586
    var addr = bitcoin.address.fromOutputScript(spk, this.network);
587
588
    return {
589
        witnessScript: ws,
590
        redeemScript: rs,
591
        scriptPubKey: spk,
592
        address: addr
593
    };
594
};
595
596
/**
597
 * get primary public key by path
598
 *  first level of the path is used as keyIndex to find the correct key in the dict
599
 *
600
 * @param path  string
601
 * @returns {bitcoin.HDNode}
602
 */
603
Wallet.prototype.getPrimaryPublicKey = function(path) {
604
    var self = this;
605
606
    path = path.replace("m", "M");
607
608
    var keyIndex = path.split("/")[1].replace("'", "");
609
610
    if (!self.primaryPublicKeys[keyIndex]) {
611
        if (self.primaryPrivateKey) {
612
            self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m");
613
        } else {
614
            throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us");
615
        }
616
    }
617
618
    var primaryPublicKey = self.primaryPublicKeys[keyIndex];
619
    return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'");
620
};
621
622
/**
623
 * get blocktrail public key by path
624
 *  first level of the path is used as keyIndex to find the correct key in the dict
625
 *
626
 * @param path  string
627
 * @returns {bitcoin.HDNode}
628
 */
629
Wallet.prototype.getBlocktrailPublicKey = function(path) {
630
    var self = this;
631
632
    path = path.replace("m", "M");
633
634
    var keyIndex = path.split("/")[1].replace("'", "");
635
636
    if (!self.blocktrailPublicKeys[keyIndex]) {
637
        throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us");
638
    }
639
640
    var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex];
641
642
    return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'");
643
};
644
645
/**
646
 * upgrade wallet to different blocktrail cosign key
647
 *
648
 * @param keyIndex  int
649
 * @param [cb]      function
650
 * @returns {q.Promise}
651
 */
652
Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) {
653
    var self = this;
654
655
    var deferred = q.defer();
656
    deferred.promise.nodeify(cb);
657
658
    if (self.locked) {
659
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index"));
660
        return deferred.promise;
661
    }
662
663
    var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered();
664
665
    deferred.resolve(
666
        self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"])
667
            .then(function(result) {
668
                self.keyIndex = keyIndex;
669
                _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) {
670
                    self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network);
671
                });
672
673
                self.primaryPublicKeys[keyIndex] = primaryPublicKey;
674
675
                return true;
676
            })
677
    );
678
679
    return deferred.promise;
680
};
681
682
/**
683
 * generate a new derived private key and return the new address for it
684
 *
685
 * @param [chainIdx] int
686
 * @param [cb]  function        callback(err, address)
687
 * @returns {q.Promise}
688
 */
689
Wallet.prototype.getNewAddress = function(chainIdx, cb) {
690
    var self = this;
691
692
    // chainIdx is optional
693
    if (typeof chainIdx === "function") {
694
        cb = chainIdx;
695
        chainIdx = null;
696
    }
697
    // default chainIdx to self.chain
698
    if (typeof chainIdx === "undefined" || chainIdx === null) {
699
        chainIdx = self.chain;
700
    }
701
702
    var deferred = q.defer();
703
    deferred.promise.spreadNodeify(cb);
704
705
    deferred.resolve(
706
        self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx)
707
            .then(function(newDerivation) {
708
                var path = newDerivation.path;
709
                var address = newDerivation.address;
710
                if (!self.bypassNewAddressCheck) {
711
712
                    address = self.getAddressByPath(newDerivation.path);
713
                    // debug check
714
                    if (address !== newDerivation.address) {
715
                        throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + address + "]");
716
                    }
717
                }
718
719
                return [address, path];
720
            })
721
    );
722
723
    return deferred.promise;
724
};
725
726
/**
727
 * get the balance for the wallet
728
 *
729
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
730
 * @returns {q.Promise}
731
 */
732
Wallet.prototype.getBalance = function(cb) {
733
    var self = this;
734
735
    var deferred = q.defer();
736
    deferred.promise.spreadNodeify(cb);
737
738
    deferred.resolve(
739
        self.sdk.getWalletBalance(self.identifier)
740
            .then(function(result) {
741
                return [result.confirmed, result.unconfirmed];
742
            })
743
    );
744
745
    return deferred.promise;
746
};
747
748
/**
749
 * get the balance for the wallet
750
 *
751
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
752
 * @returns {q.Promise}
753
 */
754
Wallet.prototype.getInfo = function(cb) {
755
    var self = this;
756
757
    var deferred = q.defer();
758
    deferred.promise.spreadNodeify(cb);
759
760
    deferred.resolve(
761
        self.sdk.getWalletBalance(self.identifier)
762
    );
763
764
    return deferred.promise;
765
};
766
767
/**
768
 * do wallet discovery (slow)
769
 *
770
 * @param [gap] int             gap limit
771
 * @param [cb]  function        callback(err, confirmed, unconfirmed)
772
 * @returns {q.Promise}
773
 */
774
Wallet.prototype.doDiscovery = function(gap, cb) {
775
    var self = this;
776
777
    if (typeof gap === "function") {
778
        cb = gap;
779
        gap = null;
780
    }
781
782
    var deferred = q.defer();
783
    deferred.promise.spreadNodeify(cb);
784
785
    deferred.resolve(
786
        self.sdk.doWalletDiscovery(self.identifier, gap)
787
            .then(function(result) {
788
                return [result.confirmed, result.unconfirmed];
789 View Code Duplication
            })
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
790
    );
791
792
    return deferred.promise;
793
};
794
795
/**
796
 *
797
 * @param [force]   bool            ignore warnings (such as non-zero balance)
798
 * @param [cb]      function        callback(err, success)
799
 * @returns {q.Promise}
800
 */
801
Wallet.prototype.deleteWallet = function(force, cb) {
802
    var self = this;
803
804
    if (typeof force === "function") {
805
        cb = force;
806
        force = false;
807
    }
808
809
    var deferred = q.defer();
810
    deferred.promise.nodeify(cb);
811
812
    if (self.locked) {
813
        deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet"));
814
        return deferred.promise;
815
    }
816
817
    var checksum = self.primaryPrivateKey.getAddress();
818
    var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32);
819
    var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64');
820
821
    deferred.resolve(
822
        self.sdk.deleteWallet(self.identifier, checksum, signature, force)
823
            .then(function(result) {
824
                return result.deleted;
825
            })
826
    );
827
828
    return deferred.promise;
829
};
830
831
/**
832
 * create, sign and send a transaction
833
 *
834
 * @param pay                   array       {'address': (int)value}     coins to send
835
 * @param [changeAddress]       bool        change address to use (auto generated if NULL)
836
 * @param [allowZeroConf]       bool        allow zero confirmation unspent outputs to be used in coin selection
837
 * @param [randomizeChangeIdx]  bool        randomize the index of the change output (default TRUE, only disable if you have a good reason to)
838
 * @param [feeStrategy]         string      defaults to Wallet.FEE_STRATEGY_OPTIMAL
839
 * @param [twoFactorToken]      string      2FA token
840
 * @param options
841
 * @param [cb]                  function    callback(err, txHash)
842
 * @returns {q.Promise}
843
 */
844
Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) {
845
846
    /* jshint -W071 */
847
    var self = this;
848
849
    if (typeof changeAddress === "function") {
850
        cb = changeAddress;
851
        changeAddress = null;
852
    } else if (typeof allowZeroConf === "function") {
853
        cb = allowZeroConf;
854
        allowZeroConf = false;
855
    } else if (typeof randomizeChangeIdx === "function") {
856
        cb = randomizeChangeIdx;
857
        randomizeChangeIdx = true;
858
    } else if (typeof feeStrategy === "function") {
859
        cb = feeStrategy;
860
        feeStrategy = null;
861
    } else if (typeof twoFactorToken === "function") {
862
        cb = twoFactorToken;
863
        twoFactorToken = null;
864
    } else if (typeof options === "function") {
865
        cb = options;
866
        options = {};
867 View Code Duplication
    }
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
868
869
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
870
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
871
    options = options || {};
872
    var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true;
873
874
    var deferred = q.defer();
875
    deferred.promise.nodeify(cb);
876
877
    if (self.locked) {
878
        deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins"));
879
        return deferred.promise;
880
    }
881
882
    q.nextTick(function() {
883
        deferred.notify(Wallet.PAY_PROGRESS_START);
884
        self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options)
885
            .then(
886
            function(r) { return r; },
887
            function(e) { deferred.reject(e); },
888
            function(progress) {
889
                deferred.notify(progress);
890
            }
891
        )
892
            .spread(
893
            function(tx, utxos) {
894
895
                deferred.notify(Wallet.PAY_PROGRESS_SEND);
896
897
                var data = {
898
                    signed_transaction: tx.toHex(),
899
                    base_transaction: tx.__toBuffer(null, null, false).toString('hex')
900
                };
901
902
                return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost)
903
                    .then(function(result) {
904
                        deferred.notify(Wallet.PAY_PROGRESS_DONE);
905
906
                        if (!result || !result['complete'] || result['complete'] === 'false') {
907
                            deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction"));
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
908
                        } else {
909
                            return result['txid'];
910
                        }
911
                    });
912
            },
913
            function(e) {
914
                throw e;
915
            }
916
        )
917
            .then(
918
            function(r) { deferred.resolve(r); },
919
            function(e) { deferred.reject(e); }
920
        )
921
        ;
922
    });
923
924
    return deferred.promise;
925
};
926
927
Wallet.prototype.decodeAddress = function(address) {
928
    return Wallet.getAddressAndType(address, this.network);
929
};
930
931
Wallet.getAddressAndType = function(address, network) {
932
    var addr;
933
    var type;
934
    var err;
935
936
    if (network === bitcoin.networks.bitcoin || network === bitcoin.networks.testnet) {
937
        try {
938
            addr = bitcoin.address.fromBech32(address, network);
939
            err = null;
940
            type = "bech32";
941
942
        } catch (_err) {
943
            err = _err;
944
        }
945
946
        if (!err) {
947
            // Valid bech32 but invalid network immediately alerts
948
            if (addr.prefix !== network.bech32) {
949
                throw new blocktrail.InvalidAddressError("Address invalid on this network");
950
            }
951
        }
952
    }
953
954
    if (!addr) {
955
        try {
956
            addr = bitcoin.address.fromBase58Check(address);
957
            err = null;
958
            type = "base58";
959
        } catch (_err) {
960
            err = _err;
961
        }
962
963
        if (!err) {
964
            // Valid base58 but invalid network immediately alerts
965
            if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) {
966
                throw new blocktrail.InvalidAddressError("Address invalid on this network");
967
            }
968
        }
969
    }
970
971
    if (err) {
972
        throw new blocktrail.InvalidAddressError(err.message);
973
    }
974
975
    return {
976
        address: address,
977
        decoded: addr,
978
        type: type
0 ignored issues
show
Bug introduced by
The variable type seems to not be initialized for all possible execution paths.
Loading history...
979
    };
980
};
981
982
Wallet.convertPayToOutputs = function(pay, network) {
983
    var send = [];
984
985
    var readFunc;
986
987
    // Deal with two different forms
988
    if (Array.isArray(pay)) {
989
        // output[]
990
        readFunc = function(i, output, obj) {
991
            if (typeof output !== "object") {
992
                throw new Error("Invalid transaction output for numerically indexed list [1]");
993
            }
994
995
            var keys = Object.keys(output);
996
            if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) {
997
                obj.scriptPubKey = output["scriptPubKey"];
998
                obj.value = output["value"];
999
            } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) {
1000
                obj.address = output["address"];
1001
                obj.value = output["value"];
1002
            } else if (keys.length !== 2 && output.length !== 2 && keys[0] !== 0 && keys[1] !== 1) {
1003
                obj.address = output[0];
1004
                obj.value = output[1];
1005
            } else {
1006
                throw new Error("Invalid transaction output for numerically indexed list [2]");
1007
            }
1008
        };
1009
    } else if (typeof pay === "object") {
1010
        // map[addr]amount
1011
        readFunc = function(address, value, obj) {
1012
            obj.address = address.trim();
1013
            obj.value = value;
1014
            if (obj.address === Wallet.OP_RETURN) {
1015
                var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1016
                obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex');
1017
                obj.value = 0;
1018
                obj.address = null;
1019
            }
1020
        };
1021
    } else {
1022
        throw new Error("Invalid input");
1023
    }
1024
1025
    Object.keys(pay).forEach(function(key) {
1026
        var obj = {};
1027
        readFunc(key, pay[key], obj);
1028
1029
        if (parseInt(obj.value, 10).toString() !== obj.value.toString()) {
1030
            throw new blocktrail.WalletSendError("Values should be in Satoshis");
1031
        }
1032
1033
        // Remove address, replace with scriptPubKey
1034
        if (typeof obj.address === "string") {
1035
            try {
1036
                var addrAndType = Wallet.getAddressAndType(obj.address, network);
1037
                obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network).toString('hex');
1038
                delete obj.address;
1039
            } catch (e) {
1040
                throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")");
1041
            }
1042
        }
1043
1044
        // Extra checks when the output isn't OP_RETURN
1045
        if (obj.scriptPubKey.slice(0, 2) !== "6a") {
1046
            if (!(obj.value = parseInt(obj.value, 10))) {
1047
                throw new blocktrail.WalletSendError("Values should be non zero");
1048
            } else if (obj.value <= blocktrail.DUST) {
1049
                throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")");
1050
            }
1051
        }
1052
1053
        // Value fully checked now
1054
        obj.value = parseInt(obj.value, 10);
1055
1056
        send.push(obj);
1057
    });
1058
1059
    return send;
1060
};
1061
1062
Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) {
1063
    /* jshint -W071 */
1064
    var self = this;
1065
1066
    if (typeof changeAddress === "function") {
1067
        cb = changeAddress;
1068
        changeAddress = null;
1069
    } else if (typeof allowZeroConf === "function") {
1070
        cb = allowZeroConf;
1071
        allowZeroConf = false;
1072
    } else if (typeof randomizeChangeIdx === "function") {
1073
        cb = randomizeChangeIdx;
1074
        randomizeChangeIdx = true;
1075
    } else if (typeof feeStrategy === "function") {
1076
        cb = feeStrategy;
1077
        feeStrategy = null;
1078
    } else if (typeof options === "function") {
1079
        cb = options;
1080
        options = {};
1081
    }
1082
1083
    randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true;
1084
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1085
    options = options || {};
1086
1087
    var deferred = q.defer();
1088
    deferred.promise.spreadNodeify(cb);
1089
1090
    q.nextTick(function() {
1091
        var send;
1092
        try {
1093
            send = Wallet.convertPayToOutputs(pay, self.network);
1094
        } catch (e) {
1095
            deferred.reject(e);
1096
            return deferred.promise;
1097
        }
1098
1099
        if (!send.length) {
1100
            deferred.reject(new blocktrail.WalletSendError("Need at least one recipient"));
1101
            return deferred.promise;
1102
        }
1103
1104
        deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION);
1105
1106
        deferred.resolve(
1107
            self.coinSelection(send, true, allowZeroConf, feeStrategy, options)
1108
            /**
1109
             *
1110
             * @param {Object[]} utxos
1111
             * @param fee
1112
             * @param change
1113
             * @param randomizeChangeIdx
0 ignored issues
show
Documentation introduced by
The parameter randomizeChangeIdx does not exist. Did you maybe forget to remove this comment?
Loading history...
1114
             * @returns {*}
1115
             */
1116
                .spread(function(utxos, fee, change) {
1117
                    var tx, txb, outputs = [];
1118
1119
                    var deferred = q.defer();
1120
1121
                    async.waterfall([
1122
                        /**
1123
                         * prepare
1124
                         *
1125
                         * @param cb
1126
                         */
1127
                        function(cb) {
1128
                            var inputsTotal = utxos.map(function(utxo) {
1129
                                return utxo['value'];
1130
                            }).reduce(function(a, b) {
1131
                                return a + b;
1132
                            });
1133
                            var outputsTotal = send.map(function(output) {
1134
                                return output.value;
1135
                            }).reduce(function(a, b) {
1136
                                return a + b;
1137
                            });
1138
                            var estimatedChange = inputsTotal - outputsTotal - fee;
1139
1140
                            if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) {
1141
                                return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " +
1142
                                    "suggested by the coin selection seems incorrect (" + estimatedChange + ")"));
1143
                            }
1144
1145
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1146
                        },
1147
                        /**
1148
                         * init transaction builder
1149
                         *
1150
                         * @param cb
1151
                         */
1152
                        function(cb) {
1153
                            txb = new bitcoin.TransactionBuilder(self.network);
1154
                            if (self.bitcoinCash) {
1155
                                txb.enableBitcoinCash();
1156
                            }
1157
1158
                            cb();
1159
                        },
1160
                        /**
1161
                         * add UTXOs as inputs
1162
                         *
1163
                         * @param cb
1164
                         */
1165
                        function(cb) {
1166
                            var i;
1167
1168
                            for (i = 0; i < utxos.length; i++) {
1169
                                txb.addInput(utxos[i]['hash'], utxos[i]['idx']);
1170
                            }
1171
1172
                            cb();
1173
                        },
1174
                        /**
1175
                         * build desired outputs
1176
                         *
1177
                         * @param cb
1178
                         */
1179
                        function(cb) {
1180
                            send.forEach(function(_send) {
1181
                                if (_send.address) {
1182
                                    outputs.push({address: _send.address, value: _send.value});
1183
                                } else if (_send.scriptPubKey) {
1184
                                    outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value});
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1185
                                } else {
1186
                                    throw new Error("Invalid send");
1187
                                }
1188
                            });
1189
                            cb();
1190
                        },
1191
                        /**
1192
                         * get change address if required
1193
                         *
1194
                         * @param cb
1195
                         */
1196
                        function(cb) {
1197
                            if (change > 0) {
1198
                                if (change <= blocktrail.DUST) {
1199
                                    change = 0; // don't do a change output if it would be a dust output
1200
1201
                                } else {
1202
                                    if (!changeAddress) {
1203
                                        deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS);
1204
1205
                                        return self.getNewAddress(self.changeChain, function(err, address) {
1206
                                            if (err) {
1207
                                                return cb(err);
1208
                                            }
1209
                                            changeAddress = address;
1210
                                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1211
                                        });
1212
                                    }
1213
                                }
1214
                            }
1215
1216
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1217
                        },
1218
                        /**
1219
                         * add change to outputs
1220
                         *
1221
                         * @param cb
1222
                         */
1223
                        function(cb) {
1224
                            if (change > 0) {
1225
                                if (randomizeChangeIdx) {
1226
                                    outputs.splice(_.random(0, outputs.length), 0, {
1227
                                        address: changeAddress,
1228
                                        value: change
1229
                                    });
1230
                                } else {
1231
                                    outputs.push({address: changeAddress, value: change});
1232
                                }
1233
                            }
1234
1235
                            cb();
1236
                        },
1237
                        /**
1238
                         * add outputs to txb
1239
                         *
1240
                         * @param cb
1241
                         */
1242
                        function(cb) {
1243
                            outputs.forEach(function(outputInfo) {
1244
                                txb.addOutput(outputInfo.scriptPubKey || outputInfo.address, outputInfo.value);
1245
                            });
1246
1247
                            cb();
1248
                        },
1249
                        /**
1250
                         * sign
1251
                         *
1252
                         * @param cb
1253
                         */
1254
                        function(cb) {
1255
                            var i, privKey, path, redeemScript, witnessScript;
1256
1257
                            deferred.notify(Wallet.PAY_PROGRESS_SIGN);
1258
1259
                            for (i = 0; i < utxos.length; i++) {
1260
                                var mode = SignMode.SIGN;
1261
                                if (utxos[i].sign_mode) {
1262
                                    mode = utxos[i].sign_mode;
1263
                                }
1264
1265
                                redeemScript = null;
0 ignored issues
show
Unused Code introduced by
The assignment to redeemScript seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
1266
                                witnessScript = null;
1267
                                if (mode === SignMode.SIGN) {
1268
                                    path = utxos[i]['path'].replace("M", "m");
1269
1270
                                    // todo: regenerate scripts for path and compare for utxo (paranoid mode)
1271
                                    if (self.primaryPrivateKey) {
1272
                                        privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair;
1273
                                    } else if (self.backupPrivateKey) {
1274
                                        privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair;
1275
                                    } else {
1276
                                        throw new Error("No master privateKey present");
1277
                                    }
1278
1279
                                    redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1280
                                    if (typeof utxos[i]['witness_script'] === 'string') {
1281
                                        witnessScript = new Buffer(utxos[i]['witness_script'], 'hex');
1282
                                    }
1283
1284
                                    var sigHash = bitcoin.Transaction.SIGHASH_ALL;
1285
                                    if (self.bitcoinCash) {
1286
                                        sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143;
1287
                                    }
1288
1289
                                    txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript);
1290
                                }
1291
                            }
1292
1293
                            tx = txb.buildIncomplete();
1294
1295
                            cb();
1296
                        },
1297
                        /**
1298
                         * estimate fee to verify that the API is not providing us wrong data
1299
                         *
1300
                         * @param cb
1301
                         */
1302
                        function(cb) {
1303
                            var estimatedFee = Wallet.estimateVsizeFee(tx, utxos);
1304
1305
                            if (self.sdk.feeSanityCheck) {
1306
                                switch (feeStrategy) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
1307
                                    case Wallet.FEE_STRATEGY_BASE_FEE:
0 ignored issues
show
Bug introduced by
The variable Wallet seems to be never declared. If this is a global, consider adding a /** global: Wallet */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
1308
                                        if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) {
1309
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1310
                                                "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE"));
1311
                                        }
1312
                                    break;
1313
1314
                                    case Wallet.FEE_STRATEGY_OPTIMAL:
1315
                                        if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) {
1316
                                            return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " +
1317
                                                "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL"));
1318
                                        }
1319
                                    break;
1320
                                }
1321
                            }
1322
1323
                            cb();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1324
                        }
1325
                    ], function(err) {
1326
                        if (err) {
1327
                            deferred.reject(new blocktrail.WalletSendError(err));
1328
                            return;
1329
                        }
1330
1331
                        deferred.resolve([tx, utxos]);
1332
                    });
1333
1334
                    return deferred.promise;
1335
                }
1336
            )
1337
        );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1338
    });
1339
1340
    return deferred.promise;
1341
};
1342
1343
1344
/**
1345
 * use the API to get the best inputs to use based on the outputs
1346
 *
1347
 * @param pay               array       {'address': (int)value}     coins to send
1348
 * @param [lockUTXO]        bool        lock UTXOs for a few seconds to allow for transaction to be created
1349
 * @param [allowZeroConf]   bool        allow zero confirmation unspent outputs to be used in coin selection
1350
 * @param [feeStrategy]     string      defaults to FEE_STRATEGY_OPTIMAL
1351
 * @param [options]         object
1352
 * @param [cb]              function    callback(err, utxos, fee, change)
1353
 * @returns {q.Promise}
1354
 */
1355
Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) {
1356
    var self = this;
1357
1358
    if (typeof lockUTXO === "function") {
1359
        cb = lockUTXO;
1360
        lockUTXO = true;
1361
    } else if (typeof allowZeroConf === "function") {
1362
        cb = allowZeroConf;
1363
        allowZeroConf = false;
1364
    } else if (typeof feeStrategy === "function") {
1365
        cb = feeStrategy;
1366
        feeStrategy = null;
1367
    } else if (typeof options === "function") {
1368
        cb = options;
1369
        options = {};
1370
    }
1371
1372
    lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true;
1373
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1374
    options = options || {};
1375
1376
    var send;
1377
    try {
1378
        send = Wallet.convertPayToOutputs(pay, self.network);
1379
    } catch (e) {
1380
        var deferred = q.defer();
1381
        deferred.promise.nodeify(cb);
1382
        deferred.reject(e);
1383
        return deferred.promise;
1384
    }
1385
1386
    return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb);
1387
};
1388
1389
/**
1390
 * send the transaction using the API
1391
 *
1392
 * @param txHex             string      partially signed transaction as hex string
1393
 * @param paths             array       list of paths used in inputs which should be cosigned by the API
1394
 * @param checkFee          bool        when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception
1395
 * @param [twoFactorToken]  string      2FA token
1396
 * @param prioboost         bool
1397
 * @param [cb]              function    callback(err, txHash)
1398
 * @returns {q.Promise}
1399
 */
1400
Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) {
1401
    var self = this;
1402
1403
    if (typeof twoFactorToken === "function") {
1404
        cb = twoFactorToken;
1405
        twoFactorToken = null;
1406
        prioboost = false;
1407
    } else if (typeof prioboost === "function") {
1408
        cb = twoFactorToken;
1409
        prioboost = false;
1410
    }
1411
1412
    var deferred = q.defer();
1413
    deferred.promise.nodeify(cb);
1414
1415
    self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost)
1416
        .then(
1417
            function(result) {
1418
                deferred.resolve(result);
1419
            },
1420
            function(e) {
1421
                if (e.requires_2fa) {
1422
                    deferred.reject(new blocktrail.WalletMissing2FAError());
1423
                } else if (e.message.match(/Invalid two_factor_token/)) {
1424
                    deferred.reject(new blocktrail.WalletInvalid2FAError());
1425
                } else {
1426
                    deferred.reject(e);
1427
                }
1428
            }
1429
        )
1430
    ;
1431
1432
    return deferred.promise;
1433
};
1434
1435
/**
1436
 * setup a webhook for this wallet
1437
 *
1438
 * @param url           string      URL to receive webhook events
1439
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1440
 * @param [cb]          function    callback(err, webhook)
1441
 * @returns {q.Promise}
1442
 */
1443
Wallet.prototype.setupWebhook = function(url, identifier, cb) {
1444
    var self = this;
1445
1446
    if (typeof identifier === "function") {
1447
        cb = identifier;
1448
        identifier = null;
1449
    }
1450
1451
    identifier = identifier || ('WALLET-' + self.identifier);
1452
1453
    return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb);
1454
};
1455
1456
/**
1457
 * delete a webhook that was created for this wallet
1458
 *
1459
 * @param [identifier]  string      identifier for the webhook, defaults to WALLET- + wallet.identifier
1460
 * @param [cb]          function    callback(err, success)
1461
 * @returns {q.Promise}
1462
 */
1463
Wallet.prototype.deleteWebhook = function(identifier, cb) {
1464
    var self = this;
1465
1466
    if (typeof identifier === "function") {
1467
        cb = identifier;
1468
        identifier = null;
1469
    }
1470
1471
    identifier = identifier || ('WALLET-' + self.identifier);
1472
1473
    return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb);
1474
};
1475
1476
/**
1477
 * get all transactions for the wallet (paginated)
1478
 *
1479
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1480
 * @param [cb]      function    callback(err, transactions)
1481
 * @returns {q.Promise}
1482
 */
1483
Wallet.prototype.transactions = function(params, cb) {
1484
    var self = this;
1485
1486
    return self.sdk.walletTransactions(self.identifier, params, cb);
1487
};
1488
1489
Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) {
1490
    var self = this;
1491
1492
    if (typeof allowZeroConf === "function") {
1493
        cb = allowZeroConf;
1494
        allowZeroConf = false;
1495
    } else if (typeof feeStrategy === "function") {
1496
        cb = feeStrategy;
1497
        feeStrategy = null;
1498
    } else if (typeof options === "function") {
1499
        cb = options;
1500
        options = {};
1501
    }
1502
1503
    if (typeof allowZeroConf === "object") {
1504
        options = allowZeroConf;
1505
        allowZeroConf = false;
1506
    } else if (typeof feeStrategy === "object") {
1507
        options = feeStrategy;
1508
        feeStrategy = null;
1509
    }
1510
1511
    options = options || {};
1512
1513
    if (typeof options.allowZeroConf !== "undefined") {
1514
        allowZeroConf = options.allowZeroConf;
1515
    }
1516
    if (typeof options.feeStrategy !== "undefined") {
1517
        feeStrategy = options.feeStrategy;
1518
    }
1519
1520
    feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL;
1521
1522
    return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb);
1523
};
1524
1525
/**
1526
 * get all addresses for the wallet (paginated)
1527
 *
1528
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1529
 * @param [cb]      function    callback(err, addresses)
1530
 * @returns {q.Promise}
1531
 */
1532
Wallet.prototype.addresses = function(params, cb) {
1533
    var self = this;
1534
1535
    return self.sdk.walletAddresses(self.identifier, params, cb);
1536
};
1537
1538
/**
1539
 * @param address   string      the address to label
1540
 * @param label     string      the label
1541
 * @param [cb]      function    callback(err, res)
1542
 * @returns {q.Promise}
1543
 */
1544
Wallet.prototype.labelAddress = function(address, label, cb) {
1545
    var self = this;
1546
1547
    return self.sdk.labelWalletAddress(self.identifier, address, label, cb);
1548
};
1549
1550
/**
1551
 * get all UTXOs for the wallet (paginated)
1552
 *
1553
 * @param [params]  object      pagination: {page: 1, limit: 20, sort_dir: 'asc'}
1554
 * @param [cb]      function    callback(err, addresses)
1555
 * @returns {q.Promise}
1556
 */
1557
Wallet.prototype.utxos = function(params, cb) {
1558
    var self = this;
1559
1560
    return self.sdk.walletUTXOs(self.identifier, params, cb);
1561
};
1562
1563
Wallet.prototype.unspentOutputs = Wallet.prototype.utxos;
1564
1565
/**
1566
 * sort list of pubkeys to be used in a multisig redeemscript
1567
 *  sorted in lexicographical order on the hex of the pubkey
1568
 *
1569
 * @param pubKeys   {bitcoin.HDNode[]}
1570
 * @returns string[]
1571
 */
1572
Wallet.sortMultiSigKeys = function(pubKeys) {
1573
    pubKeys.sort(function(key1, key2) {
1574
        return key1.toString('hex').localeCompare(key2.toString('hex'));
1575
    });
1576
1577
    return pubKeys;
1578
};
1579
1580
/**
1581
 * determine how much fee is required based on the inputs and outputs
1582
 *  this is an estimation, not a proper 100% correct calculation
1583
 *
1584
 * @todo: mark deprecated in favor of estimations where UTXOS are known
1585
 * @param {bitcoin.Transaction} tx
1586
 * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation
1587
 * @returns {number}
1588
 */
1589
Wallet.estimateIncompleteTxFee = function(tx, feePerKb) {
1590
    var size = Wallet.estimateIncompleteTxSize(tx);
1591
    var sizeKB = size / 1000;
1592
    var sizeKBCeil = Math.ceil(size / 1000);
1593
1594
    if (feePerKb) {
1595
        return parseInt(sizeKB * feePerKb, 10);
1596
    } else {
1597
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1598
    }
1599
};
1600
1601
/**
1602
 * Takes tx and utxos, computing their estimated vsize,
1603
 * and uses feePerKb (or BASEFEE as default) to estimate
1604
 * the number of satoshis in fee.
1605
 *
1606
 * @param {bitcoin.Transaction} tx
1607
 * @param {Array} utxos
1608
 * @param feePerKb
1609
 * @returns {Number}
1610
 */
1611
Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) {
1612
    var vsize = SizeEstimation.estimateTxVsize(tx, utxos);
1613
    var sizeKB = vsize / 1000;
1614
    var sizeKBCeil = Math.ceil(vsize / 1000);
1615
1616
    if (feePerKb) {
1617
        return parseInt(sizeKB * feePerKb, 10);
1618
    } else {
1619
        return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10);
1620
    }
1621
};
1622
1623
/**
1624
 * determine how much fee is required based on the inputs and outputs
1625
 *  this is an estimation, not a proper 100% correct calculation
1626
 *
1627
 * @param {bitcoin.Transaction} tx
1628
 * @returns {number}
1629
 */
1630
Wallet.estimateIncompleteTxSize = function(tx) {
1631
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1632
1633
    size += tx.outs.length * 34;
1634
1635
    tx.ins.forEach(function(txin) {
1636
        var scriptSig = txin.script,
1637
            scriptType = bitcoin.script.classifyInput(scriptSig);
1638
1639
        var multiSig = [2, 3]; // tmp hardcoded
1640
1641
        // Re-classify if P2SH
1642
        if (!multiSig && scriptType === 'scripthash') {
1643
            var sigChunks = bitcoin.script.decompile(scriptSig);
1644
            var redeemScript = sigChunks.slice(-1)[0];
1645
            scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1));
1646
            scriptType = bitcoin.script.classifyInput(scriptSig);
1647
1648
            if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) {
1649
                throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input');
1650
            }
1651
1652
            // figure out M of N for multisig (code from internal usage of bitcoinjs)
1653
            if (scriptType === 'multisig') {
1654
                var rsChunks = bitcoin.script.decompile(redeemScript);
1655
                var mOp = rsChunks[0];
1656
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1657
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1658
                }
1659
1660
                var nOp = rsChunks[redeemScript.chunks.length - 2];
1661
                if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) {
1662
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1663
                }
1664
1665
                var m = mOp - (bitcoin.opcodes.OP_1 - 1);
1666
                var n = nOp - (bitcoin.opcodes.OP_1 - 1);
1667
                if (n < m) {
1668
                    throw new blocktrail.TransactionInputError("Invalid multisig redeemScript");
1669
                }
1670
1671
                multiSig = [m, n];
1672
            }
1673
        }
1674
1675
        if (multiSig) {
1676
            size += (
1677
                32 + // txhash
1678
                4 + // idx
1679
                3 + // scriptVarInt[>=253]
1680
                1 + // OP_0
1681
                ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1682
                (2 + 105) + // OP_PUSHDATA[>=75] + script
1683
                4 // sequence
1684
            );
1685
1686
        } else {
1687
            size += 32 + // txhash
1688
                4 + // idx
1689
                73 + // sig
1690
                34 + // script
1691
                4; // sequence
1692
        }
1693
    });
1694
1695
    return size;
1696
};
1697
1698
/**
1699
 * determine how much fee is required based on the amount of inputs and outputs
1700
 *  this is an estimation, not a proper 100% correct calculation
1701
 *  this asumes all inputs are 2of3 multisig
1702
 *
1703
 * @todo: mark deprecated in favor of situations where UTXOS are known
1704
 * @param txinCnt       {number}
1705
 * @param txoutCnt      {number}
1706
 * @returns {number}
1707
 */
1708
Wallet.estimateFee = function(txinCnt, txoutCnt) {
1709
    var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime
1710
1711
    size += txoutCnt * 34;
1712
1713
    size += (
1714
            32 + // txhash
1715
            4 + // idx
1716
            3 + // scriptVarInt[>=253]
1717
            1 + // OP_0
1718
            ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt
1719
            (2 + 105) + // OP_PUSHDATA[>=75] + script
1720
            4 // sequence
1721
        ) * txinCnt;
1722
1723
    var sizeKB = Math.ceil(size / 1000);
1724
1725
    return sizeKB * blocktrail.BASE_FEE;
1726
};
1727
1728
/**
1729
 * create derived key from parent key by path
1730
 *
1731
 * @param hdKey     {bitcoin.HDNode}
1732
 * @param path      string
1733
 * @param keyPath   string
1734
 * @returns {bitcoin.HDNode}
1735
 */
1736
Wallet.deriveByPath = function(hdKey, path, keyPath) {
1737
    keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M");
1738
1739
    if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") {
1740
        throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")");
1741
    }
1742
1743
    if (path[0] === "m" && keyPath[0] === "M") {
1744
        throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")");
1745
    }
1746
1747
    // if the desired path is public while the input is private
1748
    var toPublic = path[0] === "M" && keyPath[0] === "m";
1749
    if (toPublic) {
1750
        // derive the private path, convert to public when returning
1751
        path[0] = "m";
1752
    }
1753
1754
    // keyPath should be the parent parent of path
1755
    if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) {
1756
        throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")");
1757
    }
1758
1759
    // remove the part of the path we already have
1760
    path = path.substr(keyPath.length);
1761
1762
    // iterate over the chunks and derive
1763
    var newKey = hdKey;
1764
    path.replace(/^\//, "").split("/").forEach(function(chunk) {
1765
        if (!chunk) {
1766
            return;
1767
        }
1768
1769
        if (chunk.indexOf("'") !== -1) {
1770
            chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT;
1771
        }
1772
1773
        newKey = newKey.derive(parseInt(chunk, 10));
1774
    });
1775
1776
    if (toPublic) {
1777
        return newKey.neutered();
1778
    } else {
1779
        return newKey;
1780
    }
1781
};
1782
1783
module.exports = Wallet;
1784